﻿using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using NetCore.CustomAttributes;
using NetCore.Dtos;
using Newtonsoft.Json.Linq;
using NPOI.SS.UserModel;
using NPOI.SS.Util;
using NPOI.XSSF.UserModel;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Reflection;

namespace NetCore.Extensions
{
    /// <summary>
    /// 提供Excel导入导出及文件等扩展
    /// 静态类中的静态方法不占内存，普通变量也会释放
    /// 只有静态类中的静态字段常驻内存
    /// </summary>
    public class FileExtension
    {
        #region 导出excel

        /// <summary>
        /// 直接将JARRY的数据转为可导出的格式，注意JArry的字段是 Id,ParentId,Name,Total,2020,2020_01,2020_02 .......  这个方法依赖于时间轴方法：GetTime
        /// </summary>
        /// <param name="array"></param>
        /// <param name="times"></param>
        /// <returns></returns>
        public static MemoryStream SetWorkBook<T>(JArray array, List<DynamicFieldDto<T>> times) where T : struct
        {
            return SetWorkBook(array, times, true);
        }

        /// <summary>
        /// 直接将JARRY的数据转为可导出的格式，注意JArry的字段是 Id,ParentId,Name,Total,2020,2020_01,2020_02 .......  这个方法依赖于时间轴方法：GetTime
        /// </summary>
        /// <param name="array">JArray数据、页面读取的数据</param>
        /// <param name="times">时间轴</param>
        /// <param name="isShowNo">是否显示行号</param>
        /// <returns></returns>
        public static MemoryStream SetWorkBook<T>(JArray array, List<DynamicFieldDto<T>> times, bool isShowNo, bool asc = true, string orderField = "Id") where T : struct
        {
            var workbook = GetWorkbook(array, times, orderField, asc, isShowNo);

            var ms = GetWorkbookMemoryStream(workbook);

            return ms;
        }

        public static XSSFWorkbook GetWorkbook<T>(JArray array, List<DynamicFieldDto<T>> times, string orderField = "Id", bool asc = true, bool isShowNo = false)
            where T : struct
        {
            var dateline = new List<DynamicFieldDto<T>>();

            times.ForEach(n =>
            {
                n.Caption = n.Caption + "汇总";
                n.Custom = true; //标注以下汇总时间轴
                dateline.Add(n);
                if (n.Children.Any()) dateline.AddRange(n.Children);
            });
            var years = dateline.Where(d => d.Custom).ToList();
            var months = dateline.Where(d => !d.Custom).ToList();
            months = months.Any() ? months : years; //如果只有年份无月份那么数据明细列列头与year列头一致
            array = SetLevelForJArryData(array);

            //获取层级数据，根据层级数据设置列数量
            var levels = array.Select(n =>
                int.Parse(
                    n.Value<JToken>()["Level"] != null ? n.Value<JToken>()["Level"].ToString() : "0"
                )
            ).Distinct().OrderBy(n => n).ToList();


            XSSFWorkbook workbook = new XSSFWorkbook();

            //添加一个sheet
            ISheet sheet = workbook.CreateSheet("Sheet1");

            int rowIndex = 0;
            int columnIndex = 0;
            //创建第一行的列头数据
            IRow row = sheet.CreateRow(rowIndex++);

            #region  MergeRegion  第0行 有名称合计

            row.CreateCell(columnIndex++).SetCellValue("名称");
            columnIndex += levels.Max();
            row.CreateCell(columnIndex++).SetCellValue("合计");

            #endregion

            foreach (var item in years)
            {
                row.CreateCell(columnIndex).SetCellValue(item.Caption); //设置时间轴列头
                CellRangeAddress region = new CellRangeAddress(rowIndex - 1, rowIndex - 1, columnIndex,
                    columnIndex + (item.Children.Any() ? item.Children.Count - 1 : 0)); //columnIndex - 1
                columnIndex += item.Children.Any() ? item.Children.Count : 1;
                sheet.AddMergedRegion(region);
            }

            //创建第一行的列头数据+时间轴列头
            row = sheet.CreateRow(rowIndex++);
            columnIndex = 0; //重置列号

            #region   第1行与第0行合并 有名称合计列的 MergeRegion

            CellRangeAddress rowMerge = new CellRangeAddress(0, 1, columnIndex, columnIndex);
            sheet.AddMergedRegion(rowMerge);
            columnIndex++;

            ////根据层级数量插入列名
            //levels.ForEach(n =>
            //{
            //    //if (n == 0) row1.CreateCell(columnIndex++).SetCellValue("名称");
            //    //else row1.CreateCell(columnIndex++).SetCellValue("");
            //});
            //row1.CreateCell(columnIndex++).SetCellValue("合计");

            columnIndex += levels.Max();
            rowMerge = new CellRangeAddress(0, 1, columnIndex, columnIndex);
            sheet.AddMergedRegion(rowMerge);
            columnIndex++;

            #endregion

            //时间轴
            var hasMonths = dateline.Any(d => !d.Custom);
            foreach (var item in months)
            {
                row.CreateCell(columnIndex++).SetCellValue(hasMonths ? item.Caption : item.DataField);
            }

            columnIndex = 0;

            //首先循环最上级，最上级是没有ParentId的

            var orderArray = array.Where(n => string.IsNullOrWhiteSpace(n.Value<JToken>()["ParentId"].ToString())
                                              || n.Value<JToken>()["ParentId"].ToString() == "0");
            orderArray = asc
                ? orderArray.OrderBy(n => n.Value<JToken>()[orderField])
                : orderArray.OrderByDescending(n => n.Value<JToken>()[orderField]);
            foreach (var item in orderArray)
            {
                var id = item["Id"]?.ToString();
                var name = item["Name"]?.ToString();
                //var parentId = item["ParentId"]?.ToString();
                var total = double.Parse(item["Total"] != null && !string.IsNullOrWhiteSpace(item["Total"].ToString())
                    ? item["Total"].ToString()
                    : "0");

                var rowTemp = sheet.CreateRow(rowIndex++);


                var prefix = int.Parse(id) / 100;
                name = isShowNo ? prefix + "." + name : name;
                //根据层级插入数据
                levels.ForEach(n =>
                {
                    if (n == 0)
                        rowTemp.CreateCell(columnIndex++).SetCellValue(name);
                    else
                        rowTemp.CreateCell(columnIndex++).SetCellValue("");
                });

                rowTemp.CreateCell(columnIndex++).SetCellValue(total); //合计
                //这里可以添加主表的columns，循环赋值，待解决

                foreach (var date in months)
                    if (item[date.DataField] != null && !string.IsNullOrWhiteSpace(date.DataField))
                        rowTemp.CreateCell(columnIndex++)
                            .SetCellValue(double.Parse(item[date.DataField]?.ToString())); //时间轴上的数据
                    else
                        columnIndex++; //如果有空数据也要增加列号

                columnIndex = 0; //每次循环一行后，设置列号为0

                SetChildrenToken(id, array, levels, sheet, months, ref rowIndex);
            }

            SetSheetStyle(workbook, months.Count + levels.Count + 1, rowIndex, sheet,
                levels.Count); //设置样式，因为是从第三列开始的时间轴，所以设置样式也要从第三列开始
            return workbook;
        }

        /// <summary>
        /// 将workbook 写入到内存
        /// </summary>
        /// <param name="workbook"></param>
        /// <returns></returns>
        public static MemoryStream GetWorkbookMemoryStream(XSSFWorkbook workbook)
        {
            var ms = new MemoryStream();
            workbook.Write(ms);// 写入到客户端
            ms.Flush();
            ms.Seek(0, SeekOrigin.Begin);
            return ms;
        }

        public static JArray SetLevelForJArryData(JArray arr)
        {
            //1.递归循环设置层级
            void SetChildrenTokenLevel(int level, string parentId)
            {
                var selectedToken = arr.SelectTokens("$.[?(@.ParentId == " + parentId + ")]");//选择子集
                foreach (var item in selectedToken)
                {
                    item["Level"] = level.ToString();//设置层级
                    SetChildrenTokenLevel(level + 1, item["Id"].ToString());
                }
            }


            //这里不能用 data.SelectTokens("$.[?(@.Level == '0')]") 这种方式，因为ParentId有可能为0
            foreach (var item in arr
                .Where(n => string.IsNullOrWhiteSpace(n.Value<JToken>()["ParentId"].ToString()) || n.Value<JToken>()["ParentId"].ToString() == "0"))
            {
                int level = 0;
                item["Level"] = level.ToString();
                var parentId = item["Id"].ToString();
                SetChildrenTokenLevel(1, parentId);
            }
            return arr;
        }

        /// <summary>
        /// 递归循环子集的数据，插入到Excel中
        /// </summary>
        /// <param name="selectId">父级id</param>
        /// <param name="arr">数据集</param>
        /// <param name="levels">层级数据</param>
        /// <param name="sheet1"></param>
        /// <param name="dateline">时间轴</param>
        /// <param name="rowIndex">行号</param>
        /// <param name="columnIndex">列号</param>
        public static void SetChildrenToken<T>(string selectId, JArray arr, List<int> levels, ISheet sheet1, List<DynamicFieldDto<T>> dateline,
            ref int rowIndex, int columnIndex = 0) where T : struct
        {
            var selectedToken = arr.SelectTokens("$.[?(@.ParentId == " + selectId + ")]");//选择子集
            foreach (var jToken in selectedToken)
            {
                var rowtemp = sheet1.CreateRow(rowIndex++);
                var childName = jToken["Name"]?.ToString();
                var childTotal = jToken["Total"]?.ToString();
                childTotal = !string.IsNullOrWhiteSpace(childTotal) ? childTotal : "0";
                var level = jToken["Level"].ToString();
                //除Total外的其它Columns

                levels.ForEach(n =>
                {
                    rowtemp.CreateCell(columnIndex++).SetCellValue(level == n.ToString() ? childName : "");
                });

                //依次判断层级，并设置名称
                //rowtemp.CreateCell(columnIndex++).SetCellValue("");
                //rowtemp.CreateCell(columnIndex++).SetCellValue(level == "1" ? childName : "");
                //rowtemp.CreateCell(columnIndex++).SetCellValue(level == "2" ? childName : "");
                //rowtemp.CreateCell(columnIndex++).SetCellValue(level == "3" ? childName : "");
                rowtemp.CreateCell(columnIndex++).SetCellValue(double.Parse(childTotal));//合计
                //时间轴数据
                foreach (var date in dateline)
                {
                    if (jToken[date.DataField] != null && !string.IsNullOrWhiteSpace(jToken[date.DataField].ToString()))
                        rowtemp.CreateCell(columnIndex++).SetCellValue(double.Parse(jToken[date.DataField].ToString()));//时间轴上的数据
                    else
                        columnIndex++;//如果有空数据也要增加列号
                }

                columnIndex = 0;
                SetChildrenToken(jToken["Id"].ToString(), arr, levels, sheet1, dateline, ref rowIndex);
            }
        }


        /// <summary>
        /// 设置样式
        /// </summary>
        /// <param name="workbook"></param>
        /// <param name="columnIndex"></param>
        /// <param name="rowIndex"></param>
        /// <param name="sheet"></param>
        /// <param name="row1"></param>
        public static void SetSheetStyle(XSSFWorkbook workbook, int columnIndex, int rowIndex, ISheet sheet, int levelCount)
        {
            //第一列加粗
            IFont fontRow1 = workbook.CreateFont();
            fontRow1.IsBold = true;
            //IFont fontRow2 = workbook.CreateFont();

            ICellStyle styleRow1 = workbook.CreateCellStyle();
            ICellStyle styleRow2 = workbook.CreateCellStyle();
            styleRow1.SetFont(fontRow1);
            styleRow2.Alignment = NPOI.SS.UserModel.HorizontalAlignment.Right;
            IDataFormat format = workbook.CreateDataFormat();
            styleRow2.DataFormat = format.GetFormat("###,###,###,###,###,##0.000000");
            //var cellStyle = workbook.CreateCellStyle();
            //cellStyle.DataFormat = HSSFDataFormat.GetBuiltinFormat("0.00");

            IRow row1 = sheet.GetRow(0);
            IRow row2 = sheet.GetRow(1);
            //row1.RowStyle.IsLocked = true;//只能锁定样式，能控制编辑不？
            //row2.RowStyle.IsLocked = true;//只能锁定样式，能控制编辑不？

            for (int i = 0; i < columnIndex; i++)
            {
                if (row1.GetCell(i) != null)
                {
                    row1.GetCell(i).CellStyle = styleRow1;
                    row1.GetCell(i).CellStyle.Alignment = NPOI.SS.UserModel.HorizontalAlignment.Center;
                }
                if (row2.GetCell(i) != null)
                {
                    row2.GetCell(i).CellStyle = styleRow1;
                    row2.GetCell(i).CellStyle.Alignment = NPOI.SS.UserModel.HorizontalAlignment.Center;
                }

                sheet.SetColumnWidth(i, 19 * 256);//设置单元格宽度
            }

            for (int i = 2; i < rowIndex; i++)
            {
                for (int j = 1; j < levelCount; j++)
                    sheet.GetRow(i).Cells[j].CellStyle.Alignment = NPOI.SS.UserModel.HorizontalAlignment.Left;

                for (int j = levelCount; j < sheet.GetRow(i).Cells.Count; j++)
                {
                    sheet.GetRow(i).Cells[j].CellStyle = styleRow2;
                }
            }
        }

        #endregion

        #region 直接给List数据源，导出Treelist数据

        /// <summary>
        /// 直接给List数据源，导出Treelist数据 
        /// </summary>
        /// <typeparam name="T">TreeList数据源类型</typeparam>
        /// <param name="source">TreeList数据源</param>
        /// <param name="isShowNo">名称前是否显示行号</param>
        /// <param name="asc">增序/降序</param>
        /// <param name="filter">传入按某字段排序的委托</param>
        /// <returns></returns>
        public static MemoryStream SetWorkBook<T>(List<T> source, bool isShowNo, bool asc = true, Func<T, bool> filter = null) where T : BaseExportDto
        {
            source = SetLevelForSourceData(source);
            //获取层级数据，根据层级数据设置列数量
            var levels = source.Select(n => n.Level ?? 0).OrderBy(n => n).Distinct().ToList();

            XSSFWorkbook workbook = new XSSFWorkbook();

            //添加一个sheet
            ISheet sheet = workbook.CreateSheet("Sheet1");

            int rowIndex = 0;
            int columnIndex = 0;
            //创建第一行的列头数据
            IRow row = sheet.CreateRow(rowIndex++);

            #region  MergeRegion  第0行 有名称合计

            row.CreateCell(columnIndex).SetCellValue("名称");
            columnIndex += levels.Max() > 0 ? (int)levels.Max() : 1;
            //后面的列头根据反射创建
            var type = typeof(T);
            var exportCount = 0;//导出列计数
            var properties = type.GetProperties();
            foreach (var prop in properties)
            {
                //ExportAttribute 一定要与Description 成对出现
                if (prop.IsDefined(typeof(ExportAttribute), true))
                {
                    var headerName = prop.GetCustomAttribute<DescriptionAttribute>()?.Description;
                    row.CreateCell(columnIndex++).SetCellValue(headerName);
                    exportCount++;
                }
            }
            columnIndex = 0;//重置列号
            #endregion


            #region   第1行与第0行合并 有名称合计列的 MergeRegion
            //row = sheet.CreateRow(rowIndex++);
            ////CellRangeAddress rowMerge = new CellRangeAddress(0, 1, columnIndex, columnIndex);
            ////sheet.AddMergedRegion(rowMerge);
            ////columnIndex++;

            //////根据层级数量插入列名
            //levels.ForEach(n => { row.CreateCell(columnIndex++).SetCellValue(n == 0 ? "名称" : ""); });

            ////row1.CreateCell(columnIndex++).SetCellValue("合计");

            //columnIndex += levels.Max();
            //CellRangeAddress rowMerge = new CellRangeAddress(0, 1, columnIndex, columnIndex);
            //sheet.AddMergedRegion(rowMerge);
            //columnIndex++;
            #endregion

            //数据行
            //首先循环最上级，最上级是没有ParentId的 按Id升序或降序排列
            var orderSource = source.Where(m => !m.ParentId.HasValue || m.ParentId == 0);
            orderSource = asc
                ? filter == null ? orderSource.OrderBy(n => n.Id) : orderSource.OrderBy(filter)
                : filter == null ? orderSource.OrderByDescending(n => n.Id) : orderSource.OrderByDescending(filter);
            foreach (var item in orderSource)
            {
                row = sheet.CreateRow(rowIndex++);
                var id = item.Id;
                var name = item.Name;
                var prefix = id / 100;
                name = isShowNo ? prefix + "." + name : name;
                //根据层级插入数据
                levels.ForEach(n =>
                {
                    if (n == 0)
                        row.CreateCell(columnIndex++).SetCellValue(name);
                    else
                        row.CreateCell(columnIndex++).SetCellValue("");
                });

                //rowTemp.CreateCell(columnIndex++).SetCellValue(total);//合计
                //这里可以添加主表的columns，循环赋值
                columnIndex = SetCellValues(properties, row, columnIndex, item);
                #region 根据返回的columnIndex中间可扩展继续添加列 


                columnIndex = 0;//每次循环一行后，设置列号为0
                #endregion
                //设置子级行数据 递归
                SetChildrenToken(id, source, levels, properties, sheet, ref rowIndex);
            }

            SetSheetStyle(workbook, exportCount + levels.Count + 2, rowIndex, sheet, levels.Count);//设置样式，因为是从第三列开始的时间轴，所以设置样式也要从第三列开始

            MemoryStream ms = new MemoryStream();
            workbook.Write(ms);// 写入到客户端
            ms.Flush();
            ms.Seek(0, SeekOrigin.Begin);

            return ms;
        }

        /// <summary>
        /// 根据类型的properties 设置值
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="properties"></param>
        /// <param name="row"></param>
        /// <param name="columnIndex"></param>
        /// <param name="item"></param>
        /// <returns></returns>
        private static int SetCellValues<T>(PropertyInfo[] properties, IRow row, int columnIndex, T item)
            where T : BaseExportDto
        {
            foreach (var prop in properties)
            {
                //ExportAttribute 一定要与Description 成对出现
                if (prop.IsDefined(typeof(ExportAttribute), true))
                {
                    row.CreateCell(columnIndex++).SetCellValue(prop.GetValue(item)?.ToString());
                }
            }

            return columnIndex;
        }

        /// <summary>
        /// 设置层级并递归
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="source"></param>
        /// <returns></returns>
        public static List<T> SetLevelForSourceData<T>(List<T> source) where T : BaseExportDto
        {
            //1.递归循环设置层级
            void SetChildrenTokenLevel(long level, long? parentId)
            {
                var sons = source.Where(s => s.ParentId == level);//选择子集
                foreach (var item in sons)
                {
                    item.Level = level;//设置层级
                    SetChildrenTokenLevel(level + 1, item.Id);
                }
            }

            foreach (var item in source.Where(n => !n.ParentId.HasValue || n.ParentId == 0))
            {
                long level = 0;
                item.Level = level;
                SetChildrenTokenLevel(1, item.Id);
            }
            return source;
        }

        public static void SetChildrenToken<T>(long id, List<T> source, List<long> levels, PropertyInfo[] properties, ISheet sheet,
         ref int rowIndex, int columnIndex = 0) where T : BaseExportDto
        {
            var sons = source.Where(m => m.ParentId == id);//选择子集
            foreach (var item in sons)
            {
                var row = sheet.CreateRow(rowIndex++);
                var childName = item.Name;
                var level = item.Level;
                levels.ForEach(n =>
                {
                    row.CreateCell(columnIndex++).SetCellValue(level == n ? childName : "");
                });
                //除Total外的其它Columns
                columnIndex = SetCellValues(properties, row, columnIndex, item);

                #region 根据返回的columnIndex中间可扩展继续添加列 


                columnIndex = 0;//进入次循环前归0
                #endregion
                //设置子级行数据
                SetChildrenToken(item.Id, source, levels, properties, sheet, ref rowIndex);
            }
        }

        #endregion

        #region 内存流写入文件

        /// <summary>
        /// 内存流写入文件
        /// </summary>
        /// <param name="ms">内存流</param>
        /// <param name="filePath">创建文件的物理地址</param>
        public static void MemoryToFile(MemoryStream ms, string filePath)
        {
            FileStream fs = new FileStream(filePath, FileMode.Create, FileAccess.Write);
            if (ms.CanWrite)
            {
                using (fs)
                {
                    ms.WriteTo(fs);
                    fs.Flush();
                    fs.Close();
                }
                ms.Close();
            }
        }

        #endregion

        #region 根据XSSFWorkbook创建Excel文件
        /// <summary>
        /// 根据XSSFWorkbook创建Excel文件
        /// </summary>
        /// <param name="workbook">XSSFWorkbook工作簿</param>
        /// <param name="filePath">创建文件的物理地址</param>
        public static void WriteWorkbookToFile(XSSFWorkbook workbook, string filePath)
        {
            FileStream fs = new FileStream(filePath, FileMode.Create, FileAccess.Write);
            using (fs)
            {
                workbook.Write(fs);
                fs.Close();
            }
        }

        #endregion

        #region 保存文件
        /// <summary>
        /// 保存文件
        /// </summary>
        /// <param name="file">文件</param>
        /// <param name="filePath">文件物理地址</param>
        public static void SavaFile(IFormFile file, string filePath)
        {
            //File.Copy(file.FileName, filePath);//CreateNew
            using (var fs = new FileStream(filePath, FileMode.CreateNew))//, FileAccess.Write//File.Create(filePath))//
            {
                file.CopyTo(fs);//CopyToAsync
                //fs.Flush();
            }
        }

        #endregion

        #region 保存文件目录/文件
        // 检查是否要创建上传文件夹
        public static bool CreateFolderIfNeeded(string path)
        {
            bool success = true;
            if (!Directory.Exists(path))
            {
                try
                {
                    Directory.CreateDirectory(path);
                }
                catch (Exception)
                {
                    //TODO：处理异常
                    success = false;
                }
            }
            return success;
        }
        /// <summary>
        /// 清空指定目录下的文件
        /// </summary>
        /// <param name="targetPath"></param>
        public static void ClearDir(string targetPath)
        {
            if (Directory.Exists(targetPath))
            {
                //var dirInfo = new DirectoryInfo(targetPath);
                //foreach (var file in dirInfo.GetFiles())
                //{
                //    file.Delete();
                //}
                Directory.Delete(targetPath);//删除这个目录及文件
            }
        }
        /// <summary>
        /// 删除指定文件
        /// </summary>
        /// <param name="fileFullName"></param>
        public static void ClearFile(string fileFullName)
        {
            if (File.Exists(fileFullName))
            {
                File.Delete(fileFullName);
            }
        }
        /// <summary>
        /// 查找指定路劲下所有目录
        /// </summary>
        /// <param name="rootPath"></param>
        /// <returns></returns>
        public static List<DirectoryInfo> GetAllDirectories(string rootPath)
        {
            var directories = new List<DirectoryInfo>();
            if (!Directory.Exists(rootPath))
            {
                return directories;
            }
            var directory = new DirectoryInfo(rootPath);
            directories.Add(directory);
            foreach (var directoryInfo in directory.GetDirectories())
            {
                directories.AddRange(GetAllDirectories(directoryInfo.FullName));
            }

            return directories;
        }

        #endregion



        #region 搬移baseController中的文件扩展方法
        /// <summary>
        /// Returns a file with the specified <paramref name="fileContents" /> as content (<see cref="F:Microsoft.AspNetCore.Http.StatusCodes.Status200OK" />), the
        /// specified <paramref name="contentType" /> as the Content-Type and the specified <paramref name="fileDownloadName" /> as the suggested file name.
        /// This supports range requests (<see cref="F:Microsoft.AspNetCore.Http.StatusCodes.Status206PartialContent" /> or
        /// <see cref="F:Microsoft.AspNetCore.Http.StatusCodes.Status416RangeNotSatisfiable" /> if the range is not satisfiable).
        /// </summary>
        /// <param name="fileContents">The file contents.</param>
        /// <param name="contentType">The Content-Type of the file.</param>
        /// <param name="fileDownloadName">The suggested file name.</param>
        /// <returns>The created <see cref="T:Microsoft.AspNetCore.Mvc.FileContentResult" /> for the response.</returns>
        [NonAction]
        public static FileContentResult FileContentResult(byte[] fileContents, string contentType, string fileDownloadName = null)
        {
            FileContentResult fileContentResult = new FileContentResult(fileContents, contentType);
            fileContentResult.FileDownloadName = fileDownloadName;
            return fileContentResult;
        }

        /// <summary>
        /// Returns the file specified by <paramref name="physicalPath" /> (<see cref="F:Microsoft.AspNetCore.Http.StatusCodes.Status200OK" />) with the
        /// specified <paramref name="contentType" /> as the Content-Type and the
        /// specified <paramref name="fileDownloadName" /> as the suggested file name.
        /// This supports range requests (<see cref="F:Microsoft.AspNetCore.Http.StatusCodes.Status206PartialContent" /> or
        /// <see cref="F:Microsoft.AspNetCore.Http.StatusCodes.Status416RangeNotSatisfiable" /> if the range is not satisfiable).
        /// </summary>
        /// <param name="physicalPath">The path to the file. The path must be an absolute path.</param>
        /// <param name="contentType">The Content-Type of the file.</param>
        /// <param name="fileDownloadName">The suggested file name.</param>
        /// <returns>The created <see cref="T:Microsoft.AspNetCore.Mvc.PhysicalFileResult" /> for the response.</returns>
        [NonAction]
        public static PhysicalFileResult PhysicalFile(string physicalPath, string contentType, string fileDownloadName)
        {
            PhysicalFileResult physicalFileResult = new PhysicalFileResult(physicalPath, contentType);
            physicalFileResult.FileDownloadName = fileDownloadName;
            return physicalFileResult;
        }

        #endregion
    }
}
